#!/usr/bin/env python
#{{{
# Copyright (C) 2002 Michael Benfield
# This file may be modified, distributed, copied, sold, downloaded, uploaded,
# included, imported, compiled, interpreted, hacked, linked or used with only 
# one restriction: you may not take credit for it if you did not write it.
# THIS FILE IS PROVIDED WITHOUT ANY WARRANTY.
#}}}
#{{{
# imports
import socket, struct
import cStringIO as StringIO
import cPickle as Pickle
#}}}
#{{{
# globals
do_logging = 0
#}}}
#{{{
# helper functions
#{{{
def convert_object(o):
    """
Returns the pickled and packed version of the given object.
    """
    sio = StringIO.StringIO()
    Pickle.dump(o, sio)
    s = sio.getvalue()
    l = len(s)
    fmt = '!L%is' % (l)
    return struct.pack(fmt, l, s)
#}}}
#{{{
def interpret_objects(o):
    """
Returns a tuple of (string_remnants, [interpreted_objects]) where string_remnants is the portion of the string that only held an incomplete object.
    """
    if len(o) < 4:
        return (o, [])
    length, = struct.unpack('!L', o[:4])
    without_length =  o[4:]
    if len(without_length) < length:
            return (o, [])
    remainder = without_length[length:]
    pickled, = struct.unpack('!%is' % (length), without_length[:length])
    up = Pickle.Unpickler(StringIO.StringIO(pickled))
    up.find_global = None
    try:
        object = up.load()
    except:
        return (remainder, [])
    remnants, others = interpret_objects(remainder)
    others.append(object)
    return (remnants, others)
#}}}
#{{{
def _interpret_objects(o):
    """
Returns a tuple of (string_remnants, [interpreted_objects]) where string_remnants is the portion of the string that only held an incomplete object.
    """
    if len(o) < 4:
        return (o, [])
    length, = struct.unpack('!L', o[:4])
    without_length =  o[4:]
    if len(without_length) < length:
            return (o, [])
    remainder = without_length[length:]
    pickled, = struct.unpack('!%is' % (length), without_length[:length])
    try:
        object = Pickle.loads(pickled)
    except:
        return (remainder, [])
    remnants, others = interpret_objects(remainder)
    others.append(object)
    return (remnants, others)
#}}}
#{{{
class Log(object):
    """
Keep a log of network events. The events are available in list form as the attribute 'data'. 
    """
    #{{{
    def __init__(self, file=None, line_by_line=0):
        """
If you wish the events to be written to a file, pass either a filename or a file object (has a 'write' method) opened for writing. if line_by_line is true (default false), will write only the newest line to the file each time it is logged.
        """
        self.line_by_line = line_by_line
        self.data = []
        self.file = file
    #}}}
    #{{{
    def add_entry(self, entry):
        """
Append an entry to the log. May write data to a file.
        """
        self.data.append('%s\n' % entry)
        if not self.file:
            return

        if self.line_by_line:
            to_write = '%s\n' % entry
        else:
            to_write = ''.join(self.data)

        if type(self.file) == str:
            # filename
            f = file(self.file)
            f.write(to_write)
            f.close()
        else:
            # file object
            self.file.write(to_write)
            self.file.flush()
    #}}}
    #{{{
    def close(self):
        """
Close the file object. Safe to call even if a string was passed.
        """
        if type(self.file) == file:
            self.file.close()
        #}}}
#}}}
#{{{
class Validator(object):
    """
Used to determine what addresses to accept or reject. Allowed or denied addresses are available as the attributes 'allowed' or 'denied'.
    """
    #{{{
    def __init__(self):
        self.deny_by_default = 0
        self.denied = []
        self.allowed = []
        self.connections = []

        # list of addresses that have made failed attempts to connect
        self.attempts = []

        # max number of failed attempts to connect allowed before address is put in
        # denied
        self.max_attempts = None

        # max total connections before every attempt is denied
        self.max_connections = None

        # max connections per address
        self.max_connections_per = None
    #}}}
    #{{{
    def allow_only(self, *addresses):
        """
Pass it the only addresses you want to allow.
        """
        try:
            hosts = map(socket.gethostbyname, addresses)
        except socket.gaierror:
            raise NetworkError, 'could not resolve host'
        self.deny_by_default = 1
        self.allowed = hosts
    #}}}
    #{{{
    def deny_only(self, *addresses):
        """
Pass it the only addresses you want to deny.
        """
        try:
            hosts = map(socket.gethostbyname, addresses)
        except socket.gaierror:
            raise NetworkError, 'could not resolve host'
        self.deny_by_default = 0
        self.denied = hosts
    #}}}
    #{{{
    def allow_also(self, *addresses):
        """
Pass it additional addresses to allow.
        """
        try:
            hosts = map(socket.gethostbyname, addresses)
        except socket.gaierror:
            raise NetworkError, 'could not resolve host'
        self.allowed = self.allowed + hosts
    #}}}
    #{{{
    def deny_also(self, *addresses):
        """
Pass it additional addresses to deny.
        """
        try:
            hosts = map(socket.gethostbyname, addresses)
        except socket.gaierror:
            raise NetworkError, 'could not resolve host'
        self.denied = self.denied + hosts
    #}}}
    #{{{
    def add_attempt(self, address):
        """
The Validator keeps track of failed attempts to connect. It may be that the Validator
passes a connector that is later rejected for other reasons. If you wish, you may add a record of that attempt with that function. Pass the address.
        """
        self.attempts.append(address)
    #}}}
    #{{{
    def add_connection(self, address):
        """
Let the Validator know that the given address has connected.
        """
        self.connections.append(address)
    #}}}
    #{{{
    def remove_connection(self, address, remove_all=0):
        """
Let the Validator know that the given address has disconnected. If remove_all is true (default false), will remove every connection from that address. Will raise a NetworkError if the Validator is unaware of a connection from this address.
        """
        try:
            i = self.connections.index(address)
        except ValueError:
            raise NetworkError, 'no such connection ' + address
        del self.connections[i]
        if remove_all:
            while 1:
                try:
                    i = self.connections.index(address)
                except ValueError:
                    break
                del self.connections[i]
        #}}}
    #{{{
    def __call__(self, address):
        """
Pass it a host name or IP address. Will return true if host is ok, false otherwise.
        """
        try:
            address = socket.gethostbyname(address)
        except socket.gaierror:
            raise NetworkError, 'could not resolve host'

        if self.max_connections != None:
            if len(self.connections) >= self.max_connections:
                self.denied.append(address)
                return 0

        if self.max_connections_per != None:
            if self.connections.count(address) >= self.max_connections_per:
                self.denied.append(address)
                return 0

        if self.max_attempts != None:
            if self.attempts.count(address) >= self.max_attempts:
                self.denied.append(address)
                return 0

        if self.deny_by_default:
            if address in self.allowed:
                return 1
        else:
            if not address in self.denied:
                return 1

        # not validated...
        self.attempts.append(address)
        return 0
    #}}}
    #{{{
    # max_attempts
    __doc_max_attempts = """
Number of failed attempts a given address can make to connect before being always denied. Set to None to disable this check.
    """
    def get_max_attempts(self):
        return self._max_attempts
    def set_max_attempts(self, number=None):
        self._max_attempts = number
    max_attempts = property(get_max_attempts, set_max_attempts, None,
                            __doc_max_attempts)
    #}}}
    #{{{
    # max_connections
    __doc_max_connections = """
This is the total number of connections allowed. Set to None to disable this check. 
    """
    def get_max_connections(self):
        return self._max_connections
    def set_max_connections(self, number=None):
        self._max_connections = number
    max_connections = property(get_max_connections, set_max_connections, None,
                               __doc_max_connections)
    #}}}
    #{{{
    # max_connections_per
    __doc_max_connections_per = """
This is the total number of connections allowed per address. Set to None to disable this check.
    """
    def get_max_connections_per(self):
        return self._max_connections_per
    def set_max_connections_per(self, number=None):
        self._max_connections_per = number
    max_connections_per = property(get_max_connections_per,
                                   set_max_connections_per, None,
                                   __doc_max_connections_per)
    #}}}
#}}}
#}}}
#{{{
# sockets
#{{{
class Socket(object):
    """
Base class for Socket objects.
    """
    #{{{
    def __init__(self):
        if do_logging:
            self._real_tcp_send = self._real_tcp_send_log
            self._real_udp_send = self._real_udp_send_log
            self._real_tcp_receive = self._real_tcp_receive_log
            self._real_udp_receive = self._real_udp_receive_log
            if do_logging == 1:
                self.log = Log()
            else:
                self.log = Log(do_logging)
        else:
            self._real_tcp_send = self._real_tcp_send_
            self._real_udp_send = self._real_udp_send_
            self._real_tcp_receive = self._real_tcp_receive_
            self._real_udp_receive = self._real_udp_receive_

        self.finished = 0
        self.udp_bufsize = 1024
        self.tcp_bufsize = 1024
        self.udp_send_queue = ''
        self.udp_receive_queue = ''
        self.tcp_send_queue = ''
        self.tcp_receive_queue = ''
    #}}}
    #{{{
    def _real_tcp_send_(self):
        """Actually sends some queued tcp data."""
        if self.tcp_send_queue == '':
            return self
        length_sent = self.tcp_socket.send(self.tcp_send_queue)
        self.tcp_send_queue = self.tcp_send_queue[length_sent:]
        return self
    #}}}
    #{{{
    def _real_udp_send_(self):
        """Actually sends some queued udp data."""
        if self.udp_send_queue == '':
            return self
        length_sent = self.udp_socket.sendto(self.udp_send_queue, 
                                           (self.udp_remote_host,
                                            self.udp_remote_port))
        self.udp_send_queue = self.udp_send_queue[length_sent:]
        return self
    #}}}
    #{{{
    def _real_tcp_receive_(self):
        try:
            newdata = self.tcp_socket.recv(self.tcp_bufsize)
        except SocketError:
            return self
        if len(newdata) == 0:
            self.finished = 1
        else:
            self.tcp_receive_queue = ''.join((self.tcp_receive_queue, newdata))
        return self
    #}}}
    #{{{
    def _real_udp_receive_(self):
        try:
            newdata, address = self.udp_socket.recvfrom(self.udp_bufsize)
        except SocketError:
            return self

        # Determine if this is from who we are "connected" to
        if not self.udp_remote_port == address[1]:
            return self
        if not self.udp_remote_host == socket.gethostbyname(address[0]):
            return self

        if len(newdata) == 0:
            self.finished = 1
        else:
            self.udp_receive_queue = ''.join((self.udp_receive_queue, newdata))
        return self
    #}}}
    #{{{
    def _real_tcp_send_log(self):
        """logs and sends data"""
        if self.tcp_send_queue == '':
            return self
        entry = 'Preparing to send via TCP:\n' + self.tcp_send_queue
        self.log.add_entry(entry)
        return self._real_tcp_send_()
    #}}}
    #{{{
    def _real_udp_send_log(self):
        """logs and sends data"""
        if self.udp_send_queue == '':
            return self
        entry = 'Preparing to send via UDP:\n' + self.udp_send_queue
        self.log.add_entry(entry)
        return self._real_udp_send_()
    #}}}
    #{{{
    def _real_tcp_receive_log(self):
        try:
            newdata = self.tcp_socket.recv(self.tcp_bufsize)
        except SocketError:
            return self
        s = 'Received via TCP:\n' + newdata
        self.log.add_entry(s)
        if len(newdata) == 0:
            self.finished = 1
        else:
            self.tcp_receive_queue = ''.join((self.tcp_receive_queue, newdata))
        return self
    #}}}
    #{{{
    def _real_udp_receive_log(self):
        try:
            newdata, address = self.udp_socket.recvfrom(self.udp_bufsize)
        except SocketError:
            return self

        # Determine if this is from who we are "connected" to
        if not self.udp_remote_port == address[1]:
            s = ('Received data via UDP from bad address %s:\n' % str(address)) \
                + newdata
            self.log.add_entry(s)
            return self
        if not self.udp_remote_host == socket.gethostbyname(address[0]):
            s = ('Received data via UDP from bad address %s:\n' % str(address)) \
                + newdata
            self.log.add_entry(s)
            return self
        s = 'Received data via UDP\n' + newdata
        self.log.add_entry(s)

        if len(newdata) == 0:
            self.finished = 1
        else:
            self.udp_receive_queue = ''.join((self.udp_receive_queue, newdata))
        return self
    #}}}
    #{{{
    def tcp_bind(self, port):
        self.local_tcp_port = port
        self.tcp_socket.bind(('', port))
    #}}}
    #{{{
    def udp_bind(self, port):
        self.local_udp_port = port
        self.udp_socket.bind(('', port))
    #}}}
    #{{{
    def tcp_send(self, o):
        """
Send the given object. Actually puts the object on a queue to be sent when update is called.
        """
        self.tcp_send_queue = '%s%s' % (self.tcp_send_queue, convert_object(o))
        return self
    #}}}
    #{{{
    def udp_send(self, o):
        """
Send the given object. Actually puts the object on a queue to be sent when update is called.
        """
        self.udp_send_queue = '%s%s' % (self.udp_send_queue, convert_object(o))
        return self
    #}}}
    #{{{
    def tcp_receive(self):
        """
Returns a list of received objects or an empty list.
        """
        self.tcp_receive_queue, interpreted = \
             interpret_objects(self.tcp_receive_queue)
        return interpreted
    #}}}
    #{{{
    def udp_receive(self):
        """
Returns a list of received objects or an empty list.
        """
        self.udp_receive_queue, interpreted = \
             interpret_objects(self.udp_receive_queue)
        return interpreted
    #}}}
    #{{{
    def tcp_connect(self, host, port):
        """
Connect to given host and port. SocketError is raised if unable to connect.
        """
        self.tcp_socket.connect((host, port))
        self.tcp_socket.setblocking(0)
        return self
    #}}}
    #{{{
    def udp_connect(self, host, port):
        """
Note that udp sockets here don't actually connect. This really just stores the host and port... No way to tell here if the remote host is actually going to be accepting data. Will raise a SocketError if it cannot determine ip address from hostname.
        """
        self.udp_remote_host = socket.gethostbyname(host)
        self.udp_remote_port = port
        return self
    #}}}
    #{{{
    def close(self, reason='unknown'):
        """
Call when you're done with the socket. Pass it a reason why it was closed.
        """
        self.finished = 1
        self.reason = reason
        try:
            self.udp_socket.close()
        except AttributeError:
            pass
        try:
            self.tcp_socket.close()
        except AttributeError:
            pass
    #}}}
#}}}
#{{{
class TCPSocket(Socket):
    #{{{
    def __init__(self, s=None):
        """
May pass it an existing socket object (from the Python socket module)
if you wish it to use that socket rather than creating a new one.
        """
        Socket.__init__(self)
        if s != None:
            self.tcp_socket = s
        else:
            self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
        self.bind = self.tcp_bind
        self.send = self.tcp_send
        self.receive = self.tcp_receive
        self.connect = self.tcp_connect
    #}}}
    #{{{
    def update(self):
        """
This needs to be called to receive data and flush the send queue.
        """
        self._real_tcp_send()._real_tcp_receive()
        return self
    #}}}
#}}}
#{{{
class UDPSocket(Socket):
    #{{{
    def __init__(self, s=None):
        """
May pass it an existing socket object (from the Python socket module)
if you wish it to use that socket rather than creating a new one.
        """
        Socket.__init__(self)
        if s != None:
            self.udp_socket = s
        else:
            self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        self.udp_socket.setblocking(0)
        
        self.bind = self.udp_bind
        self.send = self.udp_send
        self.receive = self.udp_receive
        self.connect = self.udp_connect
    #}}}
    #{{{
    def update(self):
        """
This needs to be called to receive data and flush the send queue.
        """
        self._real_udp_send()._real_udp_receive()
        return self
    #}}}
#}}}
#{{{
class ComboSocket(Socket):
    """
Utilizes both a udp socket and a tcp socket to allow you to send reliably or 
unreliably.
    """
    #{{{
    def __init__(self, udp_socket=None, tcp_socket=None, udp_remote_host=None, 
                 need_udp=0):
        """
If need_udp is true, the ComboSocket will wait to get the remote udp information from the tcp socket. Until that information is received, no data can be sent or received on the udp socket and everything else received on the tcp socket will be ignered.. Also until that information is received, the need_udp attribute will be true. Will raise a NetworkError if it cannot find an open port for udp.
        """
        Socket.__init__(self)
        if udp_socket != None:
            self.udp_socket = udp_socket
        else:
            self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        # try to find an available port for udp... maybe there is a better way
        # to do this?
        try_port = 65533
        while try_port >= 0:
            try:
                self.udp_bind(try_port)
                break
            except SocketError, s:
                try_port -= 1
        if try_port < 0:
            raise NetworkError

        if tcp_socket != None:
            self.tcp_socket = tcp_socket
        else:
            self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        self.udp_socket.setblocking(0)

        if udp_remote_host == None:
            self.udp_remote_host = None
        else:
            self.udp_remote_host=socket.gethostbyname(udp_remote_host)
        self.need_udp = need_udp
        if need_udp:
            self.update = self.need_udp_update
        else:
            self.update = self.normal_update
    #}}}
    #{{{
    def receive(self):
        """
Gets objects from both the tcp and udp sockets.
        """
        self.tcp_receive_queue, tcp_interpreted = \
             interpret_objects(self.tcp_receive_queue)
        self.udp_receive_queue, udp_interpreted = \
             interpret_objects(self.udp_receive_queue)
        return tcp_interpreted + udp_interpreted
    #}}}
    #{{{
    def connect(self, host, port):
        self.udp_remote_host = socket.gethostbyname(host)
        self.need_udp = 1
        self.update = self.need_udp_update
        self.tcp_connect(host, port)
        self.tcp_send(self.udp_port_string + str(self.udp_socket.getsockname()[1]))
    #}}}
    #{{{
    def need_udp_update(self):
        self._real_tcp_receive()._real_tcp_send()
        o = self.receive()
        for s in o:
            if type(s) != str:
                continue
            if s.startswith(self.udp_port_string):
                port = s[len(self.udp_port_string):]
                try:
                    port = int(port)
                except:
                    continue
                if (port > 65535) or (port < 0):
                    continue
                self.udp_remote_port = port
                self.update = self.normal_update
                self.need_udp = 0
    #}}}
    #{{{
    def normal_update(self):
        """
Call this to flush queues and whatnot.
        """
        self._real_tcp_send()._real_tcp_receive()._real_udp_send()._real_udp_receive()
    #}}}
    udp_port_string = ':::udp_port:::'
#}}}
#{{{
class TCPServer(object):
    """
TCP server...
    """
    #{{{
    def __init__(self, port):
        self.finished = 0
        self.validator = Validator()
        self.port = port
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setblocking(0)
        self.socket.bind(('', port))
    #}}}
    #{{{
    def listen(self, x):
        self.socket.listen(x)
        return self
    #}}}
    #{{{
    def accept(self):
        """
Accept a tcp connection. Returns None if no connection or if the connection was rejected, otherwise returns a connected TCPSocket instance.
        """
        try:
            s, address = self.socket.accept()
        except SocketError:
            return None
        if self.validator(address[0]):
            s.setblocking(0)
            s = TCPSocket(s)
            self.validator.add_connection(address[0])
            return s
        else:
            self.validator.add_attempt(address[0])
            return None
    #}}}
    #{{{
    def close(self, reason='unknown'):
        """
Call this when you're done...
        """
        self.reason = reason
        self.socket.close()
        self.finished = 1
    #}}}
#}}}
#{{{
class ComboServer(TCPServer):
    #{{{
    def accept(self):
        """
Accept a connection from a ComboSocket.
        """
        try:
            s, address = self.socket.accept()
        except SocketError:
            return None
        if self.validator(address[0]):
            s.setblocking(0)
            s = ComboSocket(None, s, address[0], 1)
            s.tcp_send(s.udp_port_string + str(s.udp_socket.getsockname()[1])) 
            self.validator.add_connection(address[0])
            return s
        else:
            self.validator.add_attempt(address[0])
            return None
    #}}}
#}}}
#}}}
#{{{
# exceptions
#{{{
class NetworkError(Exception):
    pass
#}}}
#{{{
SocketError = socket.error
#}}}
#}}}
#{{{
# test
# If this file is run as the main module, all the functions will be tested.
#{{{
def test_convert_object():    
    print 'testing convert_object...'
    s = 'convert_object failed'
    
    data = 'hi'
    converted = convert_object(data)
    
    assert type(converted) == str, s

    print 'convert_object passed\n'
#}}}
#{{{
def test_interpret_objects():    
    print 'testing interpret_objects...'
    s = 'interpret_objects failed'
    
    data = 'hi'
    converted = convert_object(data)
    interpreted = interpret_objects(converted)
    converted2 = convert_object('hello') + convert_object(400)
    interpreted2 = interpret_objects(converted2)
    
    assert data == interpreted[1][0], s
    assert 'hello' in interpreted2[1] and 400 in interpreted2[1]
    data = Validator()
    converted = convert_object(data)

    print 'interpret_objects passed\n'
#}}}
#{{{
def test_Log():
    print 'testing Log...'
    s = 'Log failed'

    l = Log()
    l.add_entry('log1')
    l.add_entry('log2')
    assert l.data == ['log1\n', 'log2\n']
    l.close()

    print 'Log passed\n'
#}}}
#{{{
def test_Validator_allow_only():
    print 'testing Validator.allow_only...'
    s = 'Validator.allow_only failed'

    ip1 = '192.1.1.1'
    ip2 = '5.20.20.20'
    ip3 = '233.200.56.67'
    v = Validator()
    v.allow_only(ip1, ip2)
    assert not v(ip3), s
    assert v(ip1), s
    assert v(ip2), s

    print 'Validator.allow_only passed\n'
#}}}
#{{{
def test_Validator_deny_only():
    print 'testing Validator.deny_only...'
    s = 'Validator.deny_only failed'

    ip1 = '192.1.1.1'
    ip2 = '5.20.20.20'
    ip3 = '233.200.56.67'
    v = Validator()
    v.deny_only(ip1)
    assert not v(ip1), s
    assert v(ip3), s
    assert v(ip2), s

    print 'Validator.deny_only passed\n'
#}}}
#{{{
def test_Validator_deny_only():
    print 'testing Validator.deny_only...'
    s = 'Validator.deny_only failed'

    ip1 = '192.1.1.1'
    ip2 = '5.20.20.20'
    ip3 = '233.200.56.67'
    v = Validator()
    v.deny_only(ip1)
    assert not v(ip1), s
    assert v(ip3), s
    assert v(ip2), s

    print 'Validator.deny_only passed\n'
#}}}
#{{{
def test_Validator_allow_also():
    print 'testing Validator.allow_also...'
    s = 'Validator.allow_also failed'

    ip1 = '192.1.1.1'
    ip2 = '5.20.20.20'
    ip3 = '233.200.56.67'
    v = Validator()
    v.allow_only(ip1)
    v.allow_also(ip2)
    assert not v(ip3), s
    assert v(ip1), s
    assert v(ip2), s

    print 'Validator.allow_also passed\n'
#}}}
#{{{
def test_Validator_deny_also():
    print 'testing Validator.deny_also...'
    s = 'Validator.deny_also failed'

    ip1 = '192.1.1.1'
    ip2 = '5.20.20.20'
    ip3 = '233.200.56.67'
    v = Validator()
    v.deny_only(ip1)
    v.deny_also(ip2)
    assert not v(ip1), s
    assert not v(ip2), s
    assert v(ip3), s

    print 'Validator.deny_also passed\n'
#}}}
#{{{
def test_Validator_set_max_connections():
    print 'testing Validator.set_max_connections...'
    s = 'Validator.set_max_connections failed'

    ip1 = '192.1.1.1'
    ip2 = '122.4.5.1'
    v = Validator()
    v.max_connections = 3
    v.allow_only(ip1, ip2)

    assert v(ip1)
    assert v(ip2)
    v.add_connection(ip1)
    # 1
    assert v(ip1)
    assert v(ip2)
    v.add_connection(ip1)
    # 2
    assert v(ip1)
    assert v(ip2)
    v.add_connection(ip2)
    # 3
    assert not v(ip1)
    assert not v(ip2)
    v.remove_connection(ip1)
    # 2
    assert v(ip1)
    assert v(ip2)
    v.set_max_connections(2)
    assert not v(ip1)
    assert not v(ip2)

    print 'Validator.set_max_connections passed\n'
#}}}
#{{{
def test_Validator_set_max_connections_per():
    print 'testing Validator.set_max_connections_per...'
    s = 'Validator.set_max_connections_per failed'

    ip1 = '192.1.1.1'
    ip2 = '122.4.5.1'
    v = Validator()
    v.max_connections_per = 3
    v.allow_only(ip1, ip2)

    assert v(ip1)
    assert v(ip2)
    v.add_connection(ip1)
    # 1
    assert v(ip1)
    assert v(ip2)
    v.add_connection(ip1)
    # 2
    assert v(ip1)
    assert v(ip2)
    v.add_connection(ip1)
    # 3
    assert not v(ip1)
    assert v(ip2)
    v.remove_connection(ip1)
    # 2
    assert v(ip1)
    assert v(ip2)
    v.set_max_connections_per(2)
    v.add_connection(ip2)
    assert not v(ip1)
    assert v(ip2)

    print 'Validator.set_max_connections_per passed\n'
#}}}
#{{{
def test_Validator_set_max_attempts():
    print 'testing Validator.set_max_attempts...'
    s = 'Validator.set_max_attempts failed'

    ip1 = '192.1.1.1'
    ip2 = '122.4.5.1'
    v = Validator()
    v.allow_only(ip1, ip2)
    v.max_attempts = 3

    assert v(ip1)
    assert v(ip2)
    v.add_attempt(ip1)
    # 1
    assert v(ip1)
    assert v(ip2)
    v.add_attempt(ip1)
    # 2
    assert v(ip1)
    assert v(ip2)
    v.add_attempt(ip1)
    # 3
    assert not v(ip1)
    assert v(ip2)

    print 'Validator.set_max_attempts passed\n'
#}}}
#{{{
def test_Validator_remove_connection():
    print 'testing Validator.remove_connection...'
    s = 'Validator.remove_connection failed'

    ip1 = '192.1.1.1'
    ip2 = '122.4.5.1'
    v = Validator()

    v.add_connection(ip1)
    assert v.connections == [ip1]
    v.remove_connection(ip1)
    assert v.connections == []
    v.add_connection(ip1)
    v.add_connection(ip1)
    v.add_connection(ip1)
    assert v.connections == [ip1, ip1, ip1]
    v.remove_connection(ip1)
    assert v.connections == [ip1, ip1]
    v.add_connection(ip1)
    v.add_connection(ip2)
    v.remove_connection(ip1, 1)
    assert v.connections == [ip2]

    print 'Validator.remove_connection passed\n'
#}}}
#{{{
def test_UDPSocket():
    """
In theory, at least, this test code could fail and UDPSocket still be valid. Packets sent via a UDP socket are not guaranteed to reach their destination, and yet this code asserts that they do... shouldn't be a problem using 'localhost'. Also it could be possible that packets don't arrive by the time the receiving socket tries to receive them and asserts that they are as expected.
    """
    print 'testing UDPSocket...'
    s = 'UDPSocket failed'

    s1 = UDPSocket()
    s1.bind(5566)

    s2 = UDPSocket()
    s2.bind(5567)

    s1.connect('localhost', 5567)
    s2.connect('localhost', 5566)

    o1 = 'hello there'
    o2 = 542
    o3 = [1, 2, 3]
    o4 = {'hi': 1, 'hi2': 2}

    s1.send(o1)
    s1.send(o2)
    s1.send(o3)
    s1.send(o4)
    s1.update()

    s2.update()
    o = s2.receive()
    assert o1 in o, s
    assert o2 in o, s
    assert o3 in o, s
    assert o4 in o, s

    s2.send(o1)
    s2.update()

    s1.update()
    o = s1.receive()
    assert o1 in o, s
    assert not o2 in o, s
    s1.close()
    s2.close()

    print 'UDPSocket passed\n'
#}}}
#{{{
def test_TCPSocket():
    """
By necessity, also tests TCPServer.
    """
    print 'testing TCPSocket...'
    s = 'TCPSocket failed'

    server = TCPServer(8176)
    sock1 = TCPSocket()
    server.listen(1)
    sock1.connect('localhost', 8176)
    sock2 = server.accept()

    o1 = 'hi there'
    o2 = [1, 2, 3, 4]
    o3 = 501115
    sock1.send(o1)
    sock1.send(o2)
    sock1.send(o3)
    sock1.update()
    sock2.update()
    o = sock2.receive()

    assert o1 in o, s
    assert o2 in o, s
    assert o3 in o, s

    sock2.send(o1)
    sock2.update()
    sock1.update()
    o = sock1.receive()

    assert o1 in o, s
    assert not o2 in o, s

    server.close()
    sock1.close()
    sock2.close()

    print 'TCPSocket passed\n'
#}}}
#{{{
def test_ComboSocket():
    """
By necessity, also tests ComboServer.
    """
    print 'testing ComboSocket...'
    s = 'ComboSocket failed'

    server = ComboServer(6358)
    server.listen(1)
    sock1 = ComboSocket()
    sock1.connect('localhost', 6358)
    sock2 = server.accept()

    sock1.update()
    sock2.update()
    sock1.update()
    sock2.update()

    o1 = 'strriiiiing'
    o2 = ['list', 1, 2, [1, 2]]
    o3 = 900001.45

    sock1.udp_send(o1)
    sock1.udp_send(o2)
    sock1.tcp_send(o3)
    sock1.update()
    sock2.update()
    o = sock2.receive()
    print o
    assert o1 in o, s
    assert o2 in o, s
    assert o3 in o, s

    sock2.tcp_send(o2)
    sock2.udp_send(o1)
    sock2.update()
    sock1.update()
    o = sock1.receive()
    assert o1 in o, s
    assert o2 in o, s
    assert not o3 in o, s

    sock1.udp_send(o3)
    sock1.update()
    sock2.update()
    o = sock2.receive()
    assert o3 in o, s
    assert not o2 in o, s
    assert not o1 in o, s

    server.close()
    sock1.close()
    sock2.close()

    print 'ComboSocket passed\n'
#}}}
#{{{
def test():
    gs = globals()
    print '\n---\n'
    print 'testing\n'
    for f in gs:
        if f.startswith('test_'):
            gs[f]()
#}}}
#{{{
def remove_tests():
    gs = globals()
    to_remove = []
    for f in gs:
        if f.startswith('test_'):
            to_remove.append(f)
    for f in to_remove:
        del gs[f]
#}}}
if __name__ == '__main__':
    test()
else:
    remove_tests()
#}}}
